package linux;
import static com.mongodb.client.model.Aggregates.limit;
import static com.mongodb.client.model.Aggregates.sort;
import static com.mongodb.client.model.Sorts.descending;

import java.io.File;
import java.io.IOException;
import java.net.URISyntaxException;
import java.net.URL;
import java.nio.charset.StandardCharsets;
import java.nio.file.Files;
import java.nio.file.Path;
import java.time.LocalDate;
import java.time.temporal.ChronoUnit;
import java.time.temporal.TemporalUnit;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.Date;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.Executors;
import java.util.concurrent.TimeUnit;

import org.apache.commons.collections4.MapUtils;
import org.apache.commons.io.FileUtils;
import org.apache.commons.lang3.StringUtils;
import org.bson.Document;
import org.bson.conversions.Bson;

import com.fasterxml.jackson.databind.ObjectMapper;
import com.mongodb.client.MongoCollection;
import com.mongodb.client.MongoCursor;
import com.mongodb.client.MongoDatabase;

import utils.GitUtils;
import utils.GiteeUtils;
import utils.MongoUtils;
import utils.SystemUtils;


public class LinuxCommits {

	private static final String COMMITS_COLLECTION = "commits";
	
	private static final String TOP_CONTRIBUTORS_COLLECTION = "top_contributors";
	
	private static final String CONTRIBUTORS_YEARLY_COLLECTION = "contributors_yearly";

	private static final String DATABASE_NAME = "linux_commits";
	
	private static final String LINUX_GIT_DIR = "/home/ext2/git/linux_old1";

	private static ObjectMapper objMapper = new ObjectMapper();
	
	public static void main(String[] args) throws Exception {
		
		// save commits
//		saveCommits("2005-01-01", "2024-12-31");
		// delete duplicate commits
//		deleteDuplicateCommits();
		
		// query top 20 contributors
//		queryTopContributors("2023-01-01", "2024-12-31", 20, "lines");
		
		// query top 20 domains
//		var domains = queryTopDomains(2005, 2015, 50, "commits");
//		System.out.println(objMapper.writerWithDefaultPrettyPrinter().writeValueAsString(domains.stream().map(m->{
//			Map<String, Object> mm = new HashMap<>(m);
//			mm.put("email", ((List<?>)mm.remove("contributors")).get(0));
//			mm.put("logo", "");
//			return mm;
//		}).toList()));
		
		// generate contributors chart
		generateContributorsChart(2005, 2024, "dayly", "commits");
	}
	
	private static void saveCommits(String startDate, String endDate) throws Exception {
		GitUtils.saveCommits(new File(LINUX_GIT_DIR)
				, DATABASE_NAME
				, COMMITS_COLLECTION
				, LocalDate.parse(startDate)
				, LocalDate.parse(endDate));
	}
	private static void generateContributorsChart(int startYear, int endYear, String period, String orderBy) throws Exception {
		ConcurrentHashMap<String, Map<String, Object>> contributorsMap = new ConcurrentHashMap<>();
		LocalDate startDate = LocalDate.of(startYear, 1, 1);
		// get top  contributors 
		int timeStrLength = 0;
		TemporalUnit unit = null;
		long count = 0;
		switch (period) {
		case "monthly":
			timeStrLength = 7;
			unit = ChronoUnit.MONTHS;
			count = (endYear-startYear+1)*12;
			break;
		case "yearly":
			unit = ChronoUnit.YEARS;
			count = endYear-startYear+1;
			timeStrLength = 4;
			break;
		case "dayly":
			unit = ChronoUnit.DAYS;
			count = LocalDate.of(endYear, 12, 31).toEpochDay()-LocalDate.of(startYear, 1, 1).toEpochDay()+1;
			timeStrLength = 10;
			break;
		default:
			break;
		}
		final int finalTimeStrLength = timeStrLength;
		final TemporalUnit finalUnit = unit;
		var threadPool = Executors.newFixedThreadPool(5);
		for(int i=0;i<count;i++) {
			final int finalI = i;
			threadPool.execute(()->{
				
				LocalDate date = startDate.plus(finalI, finalUnit);
				List<Map<String, Object>> contributors = queryTopContributors(startDate.toString()
						, date.plus(1, finalUnit).plusDays(-1).toString()
						, 20
						, orderBy);
				String timeStr = date.toString().substring(0, finalTimeStrLength);
				for(Map<String, Object> contributor: contributors) {
					String contributorName = MapUtils.getString(contributor, "name");
					String contributorEmail = MapUtils.getString(contributor, "email");
					long contribution = MapUtils.getInteger(contributor, "count");
					var timeEmailMap = Map.of("time",timeStr,"email", contributorEmail);
					contributorsMap.compute(contributorName, (k,v)->{
						if(v != null) {
							v.put(timeStr, contribution);
							var emails = (List<Map<String, String>>)v.get("emails");
							var newEmails = new ArrayList<>(emails);
							newEmails.add(timeEmailMap);
							v.put("emails", newEmails);
							return v;
						}
						return new HashMap<>(Map.of("name", contributorName
								, timeStr, contribution
								, "emails", new ArrayList<>(Arrays.asList(timeEmailMap))
								));
					});
				}
			});
		}
		threadPool.shutdown();
		threadPool.awaitTermination(1, TimeUnit.DAYS);
		for(var contributor:contributorsMap.entrySet()) {
			Map<String, Object> value = contributor.getValue();
			var emails = (List<Map<String, String>>)value.get("emails");
			emails.sort((a,b)->a.get("time").compareTo(b.get("time")));
			value.put("email", emails.get(emails.size()-1).get("email"));
			String previousEmail = null;
			List<Map<String, String>> newEmails = new ArrayList<>();
			for(var timeEmailMap: emails) {
				var email = timeEmailMap.get("email");
				if(previousEmail == null || !previousEmail.equals(email)) {
					newEmails.add(timeEmailMap);
				}
				previousEmail = email;
			}
			value.put("emails", newEmails);
			value.put("yearlyCommits", getContributorYearlyCommits(contributor.getKey(), startYear, endYear));
			
			
		}
		String prettyContributorsJSON = objMapper.writerWithDefaultPrettyPrinter().writeValueAsString(contributorsMap);
		System.out.println(prettyContributorsJSON);
		render(startYear, endYear, period, orderBy, contributorsMap);
	}

	private static void render(int startYear, int endYear, String period, String orderBy, Map<String, Map<String, Object>> contributorsMap)
			throws URISyntaxException, IOException {
		// get all template files
		URL templatesDir = ClassLoader.getSystemResource("linuxCommits/topContributors");
		Path sourcePath = Path.of(templatesDir.toURI());
		// create temp source dir
		File tempSourceDir = Files.createTempDirectory("temp_linux_contributors").toFile();
		tempSourceDir.deleteOnExit();
		FileUtils.copyDirectory(sourcePath.toFile(), tempSourceDir);
		// render files
		File contributorsJsonFile = new File(tempSourceDir, "contributors.json");
		objMapper.writerWithDefaultPrettyPrinter().writeValue(contributorsJsonFile, contributorsMap);
		File indexHtmlFile = new File(tempSourceDir, "index.html");
		String indexHtmlContent = FileUtils.readFileToString(indexHtmlFile, StandardCharsets.UTF_8);
		indexHtmlContent = indexHtmlContent.replace("#{startYear}", ""+startYear)
				.replace("#{endYear}", ""+endYear)
				.replace("#{period}", period)
				.replace("#{orderBy}", orderBy)
				.replace("#{giteeToken}", GiteeUtils.getToken())
				.replace("#{contributorsJson}"
						, objMapper.writeValueAsString(contributorsMap));
		FileUtils.writeStringToFile(indexHtmlFile, indexHtmlContent, StandardCharsets.UTF_8);
		// open html file 
		SystemUtils.openFile(indexHtmlFile);
	}
	private static List<Map<String, Object>> queryTopDomains(int startYear, int endYear, int limit, String orderBy) {
		List<Map<String, Object>> domains = new ArrayList<>();
		MongoDatabase db = MongoUtils.getDatabase(DATABASE_NAME);
		MongoCollection<Document> commitsColl = db.getCollection(COMMITS_COLLECTION);
		List<Bson> pipeline = Arrays.asList(
				Document.parse(String.format("""
						{$match:{
						"$and":[
						{"author.email":/.*@.*/},
						{"$expr":{$gte:["$committer.date", {$toDate:"%s-01-01T00:00:00.000Z"}]}},
						{"$expr":{$lte:["$committer.date", {$toDate:"%s-12-31T23:59:59.999Z"}]}},
						{"message":{$not:/^merge/i}},
						{"message":{$not:/^revert/i}},
						{"message":{$not:/^Linux/}},
						{"message":{$not:/^auto/i}},
						{"message":{$not:/^back/i}},
						{"message":{$not:/^sync/i}},
						{"message":{$not:/^maintainer/i}}
						]
						}}
						""", startYear, endYear))
				, Document.parse("""
						{$project:
						{
						"emailDomain":{$arrayElemAt:[{$split:["$author.email","@"]},1]}
						,"stat":1
						,"author":1
						}
						}
						""")
				, Document.parse("""
						{$addFields:{
						"domainSize":{$size:{$split:["$emailDomain","."]}}
						,"domains":{$split:["$emailDomain","."]}
						}}
						""")
				, Document.parse("""
						{$addFields:{domain:{$concat:[
						{$arrayElemAt:["$domains",0]}
						,"."
						,{$arrayElemAt:["$domains",1]}]}
						}}
						""")
				, Document.parse("""
						{$set:{
						domain:{
						$switch:{
						branches:[
						{case:{$gte:[{$indexOfCP:["$emailDomain",".intel.com"]},0]},then:"intel.com"}
						,{case:{$gte:[{$indexOfCP:["$emailDomain",".ibm.com"]},0]},then:"ibm.com"}
						,{case:{$gte:[{$indexOfCP:["$emailDomain","suse.com"]},0]},then:"suse.de"}
						,{case:{$gte:[{$indexOfCP:["$emailDomain","suse.cz"]},0]},then:"suse.de"}
						,{case:{$gte:[{$indexOfCP:["$emailDomain","alibaba.com"]},0]},then:"alibaba.com"}
						,{case:{$gte:[{$indexOfCP:["$emailDomain","googlemail.com"]},0]},then:"gmail.com"}
						,{case:{$gte:[{$indexOfCP:["$emailDomain","fujitsu.com"]},0]},then:"fujitsu.com"}
						,{case:{$gte:[{$indexOfCP:["$emailDomain","qualcomm.com"]},0]},then:"qualcomm.com"}
						,{case:{$gte:[{$indexOfCP:["$emailDomain",".arm.com"]},0]},then:"arm.com"}
						,{case:{$gte:[{$indexOfCP:["$emailDomain","arm.linux"]},0]},then:"arm.com"}
						,{case:{$gte:[{$indexOfCP:["$emailDomain","free-electrons.com"]},0]},then:"bootlin.com"}
						,{case:{$gte:[{$indexOfCP:["$emailDomain","samsung.com"]},0]},then:"samsung.com"}
						]
						,default:"$domain"
						}
						}
						}}
						""")
				, Document.parse(String.format("""
						{
						$group:{
						_id:"$domain",
						authors:{$addToSet:"$author.email"},
						count:{$sum:%s}
						}
						}
						""", getSumExpr(orderBy)))
				, sort(descending("count"))
				, limit(limit)
				);
		MongoCursor<Document> cursor = commitsColl.aggregate(pipeline).allowDiskUse(true).iterator();
		System.out.printf("*** %s Top %d Linux Committer Domains***%n"
				, (startYear==endYear?(""+startYear):(startYear+"-"+endYear))
				, limit);
		int no = 1;
		while(cursor.hasNext()) {
			Document doc = cursor.next();
			String domain = doc.getString("_id");
			Integer count = doc.getInteger("count");
			List<String> authors = doc.get("authors",Collections.emptyList() );
			System.out.printf("%d.%s(authors:%d,\tcommits:%d)%n"
					, no++
					, StringUtils.rightPad(domain,20)
					, authors.size()
					, count
					);
			System.out.println(formatCount(count,25));
			domains.add(Map.of(
					"domain", domain,
					"count", count,
					"contributors", authors));
//				System.out.println(String.join("\n",commiters));
		}
		cursor.close();
		return domains;
	}
	private static List<Map<String, Object>> queryTopContributors(String startDay, String endDay, int limit, String orderBy) {
		List<Map<String, Object>> contributors = new ArrayList<>();
		MongoDatabase db = MongoUtils.getDatabase(DATABASE_NAME);
		MongoCollection<Document> commitsColl = db.getCollection(COMMITS_COLLECTION);
		MongoCollection<Document> topContributorsColl = db.getCollection(TOP_CONTRIBUTORS_COLLECTION);
		var cacheCursor = topContributorsColl.find(new Document("start", startDay)
				.append("end", endDay)
				.append("limit", limit)
				.append("orderBy", orderBy)
				.append("updatedTime", new Document("$gte", new Date(System.currentTimeMillis() - 7*24 * 60 * 60 * 1000))))
				.sort(descending("updatedTime"))
				.limit(1).iterator();
		List<Map<String, Object>> cachedContributors = null;
		if(cacheCursor.hasNext()) {
			var cache = cacheCursor.next();
			var cachedDocs = cache.getList("contributors", Document.class);
			if(cachedDocs!=null && !cachedDocs.isEmpty()) {
				cachedContributors = cachedDocs.stream().map(doc->Map.copyOf(doc)).toList();
			}
		}
		cacheCursor.close();
		if(cachedContributors!=null && !cachedContributors.isEmpty()) {
			return cachedContributors;
		}
		List<Bson> pipeline = Arrays.asList(
				
				Document.parse(String.format("""
						{$match:
						{
						"$and":[
						{$expr:{"$gte":["$committer.date",{$toDate:"%sT00:00:00.000Z"}]}},
						{$expr:{"$lte":["$committer.date",{$toDate:"%sT23:59:59.999Z"}]}},
						{"message":{$not:/^merge/i}},
						{"message":{$not:/^revert/i}},
						{"message":{$not:/^Linux/}},
						{"message":{$not:/^auto/i}},
						{"message":{$not:/^back/i}},
						{"message":{$not:/^sync/i}},
						{"message":{$not:/^maintainer/i}}
						]
						}
						}
						""", startDay, endDay))
				, Document.parse("""
						{$project:
						{
						"date":"$committer.date",
						"author":{
						"name":{
						$cond:{
						if:{$gt:[{$strLenCP:"$author.name"},0]}
						,then:"$author.name"
						,else:"$author.email"}}
						,"email":"$author.email"},
						"stat":1
						}
						}
						""")
				, sort(descending("date"))
				, Document.parse(String.format("""
						{$group:{
						_id:"$author.name"
						,email:{$first:"$author.email"}
						,count:{$sum:%s}
						}}
						""",getSumExpr(orderBy)))
				, sort(descending("count"))
				, Document.parse("""
						{$project:{_id:0,name:"$_id",email:1,count:1}}
						""")
				,limit(limit)
				);
		MongoCursor<Document> cursor = commitsColl.aggregate(pipeline).allowDiskUse(true).iterator();
		int maxContribution = 0;
		while(cursor.hasNext()) {
			Document contributor = cursor.next();
			Integer contribution = contributor.getInteger("count");
			maxContribution = Math.max(maxContribution, contribution);
			contributors.add(Map.copyOf(contributor));
		}
		System.out.println("***"+(startDay.equals(endDay)?startDay:(startDay+"-"+endDay))+" Top "+limit+" Linux Contributors***");
		int no = 1;
		for(var contributor: contributors) {
			var contribution = MapUtils.getInteger(contributor, "count");
			System.out.printf("%d.%s(%s):%d%n%s%n"
					, no++
					, StringUtils.rightPad(MapUtils.getString(contributor,"name"),25)
					, MapUtils.getString(contributor, "email")
					, contribution
					, formatCount(contribution, maxContribution<=400?4:maxContribution/400)
					);
			
		}
		cursor.close();
		if(!contributors.isEmpty()) {
			topContributorsColl.insertOne(new Document("start", startDay)
					.append("end", endDay)
					.append("limit", limit)
					.append("orderBy", orderBy)
					.append("contributors", contributors)
					.append("updatedTime", new Date()));
			
		}
		return contributors;
	}
	private static String getSumExpr(String orderBy) {
		String sumExpr = "1";
		switch(orderBy) {
		case "commits":
			sumExpr = "1";
			break;
		case "files":
			sumExpr = "\"$stat.files\"";
			break;
		case "lines":
			sumExpr = "\"$stat.total\"";
			break;
		default:
			break;
		}
		return sumExpr;
	}
	
	private static List<Map<String, Object>> getContributorYearlyCommits(String contributorName, int startYear, int endYear) {
		List<Map<String, Object>> commitsStat = new ArrayList<>();
		MongoDatabase db = MongoUtils.getDatabase(DATABASE_NAME);
		MongoCollection<Document> commitsColl = db.getCollection(COMMITS_COLLECTION);
		MongoCollection<Document> yearlyColl = db.getCollection(CONTRIBUTORS_YEARLY_COLLECTION);
		var cacheCursor = yearlyColl.find(new Document("name", contributorName)
				.append("startYear", startYear)
				.append("endYear", endYear)
				.append("updatedTime", new Document("$gte", new Date(System.currentTimeMillis() - 7*24 * 60 * 60 * 1000)))
				).sort(descending("updatedTime")).limit(1).iterator();
		List<Map<String, Object>> cachedCommits = null;
		if(cacheCursor.hasNext()) {
			cachedCommits = cacheCursor.next().getList("stat", Document.class).stream().map(doc->Map.copyOf(doc)).toList();
		}
		cacheCursor.close();
		if(cachedCommits!=null && !cachedCommits.isEmpty()) {
			return cachedCommits;
		}
		List<Document> pipeline = Arrays.asList(
				Document.parse(String.format("""
						{$match:{
						"$and":[
						{$or:[{"author.name":"%s"},{"author.email":"%s"}]},
						{$expr:{"$gte":["$committer.date",{$toDate:"%s-01-01T00:00:00.000Z"}]}},
						{$expr:{"$lte":["$committer.date",{$toDate:"%s-12-31T23:59:59.999Z"}]}},
						{"message":{$not:/^merge/i}},
						{"message":{$not:/^revert/i}},
						{"message":{$not:/^Linux/}},
						{"message":{$not:/^auto/i}},
						{"message":{$not:/^back/i}},
						{"message":{$not:/^sync/i}},
						{"message":{$not:/^maintainer/i}}
						]
						}}
						""", contributorName, contributorName, startYear, endYear)),
				Document.parse("""
						{$project:
						{
						"date":"$committer.date",
						"stat":1
						}
						}
						"""),
				Document.parse("""
						{$group:{
						_id:{$dateToString:{date:"$date",format:"%Y"}},
						commits:{$sum:1},
						lines:{$sum:"$stat.total"},
						files:{$sum:"$stat.files"}
						}}
						""")
				);
		var cursor = commitsColl.aggregate(pipeline).allowDiskUse(true).iterator();
		while(cursor.hasNext()) {
			var doc = cursor.next();
			commitsStat.add(Map.of(
					"year", doc.getString("_id")
					, "commits", doc.getInteger("commits")
					, "lines", doc.getInteger("lines")
					, "files", doc.getInteger("files")
					));
		}
		cursor.close();
		yearlyColl.insertOne(new Document("name", contributorName)
				.append("startYear", startYear)
				.append("endYear", endYear)
				.append("stat", commitsStat)
				.append("updatedTime", new Date()));
		return commitsStat;
	}
	
	private static void deleteDuplicateCommits() {
		MongoDatabase db = MongoUtils.getDatabase(DATABASE_NAME);
		MongoCollection<Document> commitsColl = db.getCollection(COMMITS_COLLECTION);
		var cursor = commitsColl.aggregate(Arrays.asList(
				Document.parse("""
						{
						    $group:{
						        _id:"$sha",
						        cnt:{$sum:1},
						        first:{$first:"$_id"}
						    }
						}
						"""),
				Document.parse("""
						{
						    $match:{
						        cnt:{$gt:1}
						    }
						}
						""")
				)).allowDiskUse(true).iterator();
		while(cursor.hasNext()) {
			Document doc = cursor.next();
			String sha = doc.getString("_id");
			var firstId = doc.getObjectId("first");
			commitsColl.deleteMany(new Document("sha", sha)
					.append("_id", new Document("$ne", firstId)));
		}
		cursor.close();
	}
	
	private static String formatCount(int count, int step) {
		int stepsPerBlock = 4* step;
		int blocks = count /stepsPerBlock;
		int remains = (count % stepsPerBlock)/step;
		String left = "";
		switch(remains) {
			case 1:
				left = "\u258E";
				break;
			case 2:
				left = "\u258C";
				break;
			case 3:
				left = "\u258A";
				break;
			default:
				break;
			
		}
		return StringUtils.repeat("\u2588", blocks) +left;
	}
	

}
